package com.ywger.service.server.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import com.ywger.dao.entity.primary.EnsRegister;
import com.ywger.dao.entity.primary.TxInfo;
import com.ywger.dao.mapper.primary.EnsRegisterMapper;
import com.ywger.dao.mapper.primary.TxInfoMapper;
import com.ywger.integration.entity.res.GasPriceRes;
import com.ywger.integration.resttemplate.RestTemplateHelper;
import com.ywger.integration.server.EthIntegration;
import com.ywger.service.config.ServerProperty;
import com.ywger.service.constants.ChainState;
import com.ywger.service.constants.EnsRegisterState;
import com.ywger.service.dto.contract.ETHRegistrarController;
import com.ywger.service.dto.request.EnsRegisterParam;
import com.ywger.service.dto.response.EnsStateRes;
import com.ywger.service.dto.response.GasPriceDto;
import com.ywger.service.server.EnsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.Utf8String;
import org.web3j.abi.datatypes.generated.Bytes32;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.WalletUtils;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.EthBlock;
import org.web3j.protocol.core.methods.response.EthBlockNumber;
import org.web3j.protocol.core.methods.response.EthEstimateGas;
import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.core.methods.response.EthTransaction;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.core.methods.response.Web3ClientVersion;
import org.web3j.protocol.exceptions.TransactionException;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.ChainId;
import org.web3j.tx.ChainIdLong;
import org.web3j.tx.RawTransactionManager;
import org.web3j.tx.TransactionManager;
import org.web3j.tx.response.QueuingTransactionReceiptProcessor;
import org.web3j.tx.response.TransactionReceiptProcessor;
import org.web3j.utils.Convert;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author chenmeng
 * @date 2021-11-12 22:00
 * @desc
 */
@Service
@Slf4j
public class EnsServiceImpl implements EnsService {

    private  Credentials credentials;

    private Web3j web3;

    @Autowired
    private ServerProperty serverProperty;


    @Autowired
    private EnsRegisterMapper ensRegisterMapper;

    @Autowired
    private TxInfoMapper txInfoMapper;


    @Autowired
    EnsCommitCallback ensCommitCallback;

    ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private EthIntegration ethIntegration;





    @PostConstruct
    public void init() {
        credentials = Credentials.create(serverProperty.getPrivateKey());
        web3 = Web3j.build(new HttpService(serverProperty.getEthClientUrl()));
        threadPoolExecutor = new ThreadPoolExecutor(10, 100, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
    }


    @Override
    public EnsStateRes checkEnsName(List<String> ensNameList) {
        String ethRegistrarAddress = serverProperty.getContractAddress();
        ETHRegistrarController ethRegistrarController = ETHRegistrarController.load(ethRegistrarAddress, web3, credentials, BigInteger.valueOf(250000), BigInteger.valueOf(250000));
        EnsStateRes ensStateRes = new EnsStateRes();
        ensStateRes.setAvailableList(new Vector<>());
        ensStateRes.setInvalidList(new Vector<>());
        ensStateRes.setUnavailableList(new Vector<>());
        ensStateRes.setErrorList(new Vector<>());
        CountDownLatch countDownLatch = new CountDownLatch(ensNameList.size());
        for (String ensName : ensNameList) {
            threadPoolExecutor.submit(() -> {
                try {
                    boolean validFlag = ethRegistrarController.valid(ensName).send();
                    if (!validFlag) {
                        ensStateRes.getInvalidList().add(ensName);
                    } else {
                        boolean availableFlag = ethRegistrarController.available(ensName).send();
                        if (availableFlag) {
                            ensStateRes.getAvailableList().add(ensName);
                        } else {
                            ensStateRes.getUnavailableList().add(ensName);
                        }
                    }
                } catch (Exception e) {
                    ensStateRes.getErrorList().add(ensName);
                }
               countDownLatch.countDown();
            });


        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return sort(ensStateRes,ensNameList);
    }


    public EnsStateRes sort(EnsStateRes ensStateRes,List<String> ensNameList){
        EnsStateRes sortEnsStateRes = new EnsStateRes();
        List<String> availableList = new Vector<>();
        List<String> invalidList = new Vector<>();
        List<String> unavailableList = new Vector<>();
        List<String> errorList = new Vector<>();
        sortEnsStateRes.setAvailableList(availableList);
        sortEnsStateRes.setInvalidList(invalidList);
        sortEnsStateRes.setUnavailableList(unavailableList);
        sortEnsStateRes.setErrorList(errorList);

        for (String ensName : ensNameList) {
            List<String> unSortAvailableList = ensStateRes.getAvailableList();
            List<String> unSortInvalidList = ensStateRes.getInvalidList();
            List<String> unSortUnavailableList = ensStateRes.getUnavailableList();
            List<String> unSortErrorList = ensStateRes.getErrorList();
            for (String unSortName : unSortAvailableList) {
                if(ensName.equals(unSortName)){
                    availableList.add(ensName);
                }
            }
            for (String unSortName : unSortInvalidList) {
                if(ensName.equals(unSortName)){
                    invalidList.add(ensName);
                }
            }
            for (String unSortName : unSortUnavailableList) {
                if(ensName.equals(unSortName)){
                    unavailableList.add(ensName);
                }
            }
            for (String unSortName : unSortErrorList) {
                if(ensName.equals(unSortName)){
                    errorList.add(ensName);
                }
            }
        }
        return sortEnsStateRes;

    }


    @Override
    public EnsStateRes register(EnsRegisterParam param) {
        List<String> ensNameList = param.getEnsNameList();
        EnsStateRes ensStateRes = checkEnsName(ensNameList);
        ensNameList = ensStateRes.getAvailableList();
        List<List<String>> ensBatchList  = ListUtil.split(ensNameList,serverProperty.getSplitListSize());
        Integer durationDay = param.getDurationDay();
        for (List<String> list : ensBatchList) {
            for (String ensName : list) {
                commitEIP1559(ensName,durationDay,false);
            }
            try {
                TimeUnit.SECONDS.sleep(serverProperty.getSleepSecond());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return ensStateRes;
    }




    @Override
    public void register() {

        List<EnsRegister> ensRegisterList = ensRegisterMapper.selectByTask();
        if (CollUtil.isEmpty(ensRegisterList)) {
            return;
        }
        for (EnsRegister ensRegister : ensRegisterList) {
            registerEIP1559(ensRegister.getEnsName());
        }
    }


    public static BigInteger getNonce(Web3j web3j, String addr) {
        try {
            EthGetTransactionCount getNonce = web3j.ethGetTransactionCount(addr, DefaultBlockParameterName.PENDING).send();
            if (getNonce == null) {
                throw new RuntimeException("net error");
            }
            return getNonce.getTransactionCount();
        } catch (IOException e) {
            throw new RuntimeException("net error");
        }
    }


    public static BigInteger getTransactionGasLimit(Web3j web3j, Transaction transaction) {
        try {
            EthEstimateGas ethEstimateGas = web3j.ethEstimateGas(transaction).send();
            if (ethEstimateGas.hasError()) {
                throw new RuntimeException(ethEstimateGas.getError().getMessage());
            }
            return ethEstimateGas.getAmountUsed();
        } catch (IOException e) {
            throw new RuntimeException("net error");
        }
    }


    /**
     * 常量池
     */
    public static final String[] POOL = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};

    /**
     * 生成字符串
     *
     * @return 生成的32位长度的16进制字符串
     */
    public static String generateNiceString() {
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < 32; i++) {
            sb.append(POOL[random.nextInt(POOL.length)]);
        }
        return sb.toString();
    }


    @Override
    public void dealPending() {
        TransactionReceiptProcessor transactionReceiptProcessor = new QueuingTransactionReceiptProcessor(web3, ensCommitCallback, 10, 2);
        List<TxInfo> txInfoList = txInfoMapper.selectPending();
        if (!CollUtil.isEmpty(txInfoList)) {
            for (TxInfo txInfo : txInfoList) {
                if(StringUtils.isEmpty(txInfo.getTxHash())){
                    log.error("交易hash为空");
                    continue;
                }
                try {
                    transactionReceiptProcessor.waitForTransactionReceipt(txInfo.getTxHash());
                } catch (Exception e) {

                }
            }
        }
    }


    public void commitEIP1559(String ensName,Integer durationDay,boolean retry) {
        long chainId = ChainIdLong.ROPSTEN;
        try {
            chainId = web3.ethChainId().send().getChainId().longValue();
        } catch (IOException e) {
            log.error("{},获取chainId 异常", ensName);
            return;
        }
        String ethRegistrarAddress = serverProperty.getContractAddress();
        String fromAddress = serverProperty.getFromAddress();
        ETHRegistrarController ethRegistrarController = ETHRegistrarController.load(ethRegistrarAddress, web3,credentials,
                 BigInteger.ZERO, BigInteger.valueOf(serverProperty.getGasLimit()));
       /* BigInteger minRegistrationDuration = null;
        try {
            minRegistrationDuration = ethRegistrarController.MIN_REGISTRATION_DURATION().send();
        } catch (Exception e) {
            log.error("{}，获取MIN_REGISTRATION_DURATION异常", ensName, e);
            return;
        }*/
       String randomText = generateNiceString();
        byte[] secret = new byte[0];
        try {
            secret = ethRegistrarController.makeCommitmentWithConfig(ensName, fromAddress, randomText.getBytes(), serverProperty.getResolverAddress(), fromAddress).send();
        } catch (Exception e) {
            log.error("{}，计算commit异常", ensName, e);
            return;
        }
        final Function function = new Function(
                ETHRegistrarController.FUNC_COMMIT,
                Arrays.<Type>asList(new Bytes32(secret)),
                Collections.<TypeReference<?>>emptyList());

        String data = FunctionEncoder.encode(function);
        TransactionReceiptProcessor transactionReceiptProcessor = new QueuingTransactionReceiptProcessor(web3, ensCommitCallback, 100, 2 * 1000);
        RawTransactionManager transactionManager = new RawTransactionManager(
                web3, credentials, chainId, transactionReceiptProcessor);
        BigInteger maxPriorFee;
        BigInteger maxFeePerGas;
        try {
           GasPriceDto gasPriceDto = getGasPriceDto(serverProperty.getTxSpeed());
           if(null == gasPriceDto){
               return;
           }
            maxPriorFee = gasPriceDto.getMaxPriorFee();
           maxFeePerGas = gasPriceDto.getMaxFee();
        } catch (Exception e) {
           log.error("{},getFee 异常",ensName);
           return;
        }
        EnsRegister ensRegister = new EnsRegister();
        if(retry){
            List<EnsRegister> registerList = ensRegisterMapper.selectByEnsName(ensName);
            if(CollectionUtils.isEmpty(registerList)){
                return;
            }
            ensRegister = registerList.get(0);
            ensRegister.setCommitSuccessTime(new Date());
            ensRegister.setUpdateTime(new Date());
            ensRegisterMapper.updateByPrimaryKey(ensRegister);
        }else{
            BigInteger registerDuration = BigInteger.valueOf(24*60*60L).multiply(BigInteger.valueOf(durationDay));

            ensRegister.setEnsName(ensName);
            ensRegister.setOwner(fromAddress);
            ensRegister.setSender(fromAddress);
            ensRegister.setResolverAddress(serverProperty.getResolverAddress());
            ensRegister.setDuration(registerDuration.longValue());
            ensRegister.setState(EnsRegisterState.COMMIT.getCode());
            ensRegister.setChainState(ChainState.PENDING.name());
            ensRegister.setChainId(serverProperty.getEthClientUrl());
            ensRegister.setDurationTime(durationDay+"");
            ensRegister.setSecretText(randomText);
            ensRegisterMapper.insert(ensRegister);
        }
        EthSendTransaction ethSendTransaction;


        try {
             ethSendTransaction = transactionManager.sendEIP1559Transaction(chainId,maxPriorFee,maxFeePerGas,BigInteger.valueOf(serverProperty.getGasLimit()),
                    ethRegistrarAddress,data,BigInteger.ZERO);
        } catch (IOException e) {
            log.error("{},commit 异常",ensName);
            return;
        }
        String txHash = ethSendTransaction.getTransactionHash();
        if(null == txHash){
            log.error("{},commit txhash 为空",ensName);
        }
        try {
            transactionReceiptProcessor.waitForTransactionReceipt(ethSendTransaction.getTransactionHash());
        } catch (Exception e) {
            log.error("{},commit 异常",ensName,e);
        }

        TxInfo txInfo = new TxInfo();
        txInfo.setChainState(ChainState.PENDING.name());
        txInfo.setEnsName(ensName);
        txInfo.setTxHash(txHash);
        txInfo.setStep(EnsRegisterState.COMMIT.getCode());
        txInfoMapper.insert(txInfo);
    }



    public void registerEIP1559(String ensName) {
        long chainId = ChainIdLong.ROPSTEN;
        try {
            chainId = web3.ethChainId().send().getChainId().longValue();
        } catch (IOException e) {
            log.error("{},获取chainId 异常", ensName);
            return;
        }
        String ethRegistrarAddress = serverProperty.getContractAddress();
        String fromAddress = serverProperty.getFromAddress();
        ETHRegistrarController ethRegistrarController = ETHRegistrarController.load(ethRegistrarAddress, web3,credentials,
                BigInteger.ZERO, BigInteger.valueOf(serverProperty.getGasLimit()));


        List<EnsRegister> ensRegisterList = ensRegisterMapper.selectByEnsName(ensName);
        if (CollUtil.isEmpty(ensRegisterList)) {
            log.error("ens {} 不存在", ensName);
            return;
        }
        EnsRegister ensRegister = ensRegisterList.get(0);
        BigInteger minPrice = null;
        try {
            minPrice = ethRegistrarController.rentPrice(ensName, BigInteger.valueOf(ensRegister.getDuration())).send();
        } catch (Exception e) {
            log.error("{}，获取rentPrice异常", ensName, e);
            return;
        }
        BigInteger actPrice = new BigDecimal(minPrice).multiply(new BigDecimal("1.1")).toBigInteger();

        ensRegister.setState(EnsRegisterState.REGISTER.getCode());
        ensRegister.setChainState(ChainState.PENDING.name());
        ensRegisterMapper.updateByPrimaryKey(ensRegister);
        final Function function = new Function(
                ETHRegistrarController.FUNC_REGISTERWITHCONFIG,
                Arrays.<Type>asList(new Utf8String(ensName),
                        new Address(fromAddress),
                        new Uint256(BigInteger.valueOf(ensRegister.getDuration())),
                        new Bytes32(ensRegister.getSecretText().getBytes()),
                        new Address(ensRegister.getResolverAddress()),
                        new Address(fromAddress)),
                Collections.<TypeReference<?>>emptyList());

        String data = FunctionEncoder.encode(function);
        TransactionReceiptProcessor transactionReceiptProcessor = new QueuingTransactionReceiptProcessor(web3, ensCommitCallback, 100, 2 * 1000);
        RawTransactionManager transactionManager = new RawTransactionManager(
                web3, credentials, chainId, transactionReceiptProcessor);
        BigInteger maxPriorFee;
        BigInteger maxFeePerGas;
        try {
            GasPriceDto gasPriceDto = getGasPriceDto(serverProperty.getTxSpeed());
            if(null == gasPriceDto){
                return;
            }
            maxPriorFee = gasPriceDto.getMaxPriorFee();
            maxFeePerGas = gasPriceDto.getMaxFee();
        } catch (Exception e) {
            log.error("{},getFee 异常",ensName);
            return;
        }
        EthSendTransaction ethSendTransaction;
        try {
            ethSendTransaction = transactionManager.sendEIP1559Transaction(chainId,maxPriorFee,maxFeePerGas,BigInteger.valueOf(serverProperty.getGasLimit()), ethRegistrarAddress,data,actPrice);
        } catch (IOException e) {
            log.error("{},commit 异常",ensName);
            return;
        }
        String txHash = ethSendTransaction.getTransactionHash();
        if(null == txHash){
            log.error("{},commit txhash 为空",ensName);
        }
        try {
            transactionReceiptProcessor.waitForTransactionReceipt(ethSendTransaction.getTransactionHash());
        } catch (Exception e) {
            log.error("{},commit 异常",ensName,e);
        }


        TxInfo txInfo = new TxInfo();
        txInfo.setChainState(ChainState.PENDING.name());
        txInfo.setEnsName(ensName);
        txInfo.setTxHash(txHash);
        txInfo.setStep(EnsRegisterState.REGISTER.getCode());
        txInfoMapper.insert(txInfo);
    }

    public static BigInteger getBaseFee(Web3j web3) throws IOException {
        BigInteger fee = BigInteger.ZERO;
        EthBlockNumber ethBlockNumber = web3.ethBlockNumber().send();
        EthBlock.Block ethBlock = web3.ethGetBlockByNumber(DefaultBlockParameter.valueOf(ethBlockNumber.getBlockNumber()), true)
                .send().getBlock();
        String baseFee = ethBlock.getBaseFeePerGas();
        fee = new BigInteger(baseFee.substring(2), 16);
        return fee;
    }

    public  static BigInteger getMaxPriorFee(Web3j web3) throws IOException {

        BigInteger fee = BigInteger.ZERO;
        EthBlockNumber ethBlockNumber = web3.ethBlockNumber().send();
        EthTransaction  ethTransaction = web3.ethGetTransactionByBlockNumberAndIndex(
                DefaultBlockParameter.valueOf(ethBlockNumber.getBlockNumber()),BigInteger.ONE)
                .send();
        try {
            org.web3j.protocol.core.methods.response.Transaction transaction = ethTransaction.getTransaction().get();
            String maxPrior = transaction.getMaxPriorityFeePerGas();
            if(null != maxPrior){
                fee = new BigInteger(maxPrior.substring(2),16);
            }
        } catch (Exception e) {
           log.error("getMaxPriorFee 未找到交易:",e);
        }
        return fee;
    }

    @Deprecated
    public static BigInteger getMaxFeePerGas(Web3j web3,BigInteger maxPriorityFee) throws IOException {
        BigInteger baseFee = getBaseFee(web3);
        return  new BigDecimal(baseFee).multiply(new BigDecimal("1.25")).toBigInteger().add(maxPriorityFee);
    }


    @Override
    public void retry(List<String> ensNameList){
        for (String ensName : ensNameList) {
            List<EnsRegister> ensRegisterList = ensRegisterMapper.selectByEnsName(ensName);
            if(CollectionUtils.isEmpty(ensNameList)){
                continue;
            }
            EnsRegister ensRegister = ensRegisterList.get(0);
           String state =  ensRegister.getState();
           String chainState = ensRegister.getChainState();
           if(!ChainState.PENDING.name().equals(chainState)){
               continue;
           }
           switch (state){
               case "COMMIT" :
                   commitEIP1559(ensName,null,true);
                   break;
               case "REGISTER" :
                   registerEIP1559(ensName);
                   break;
               default:
                   break;
           }
        }


    }

    @Override
    public Map<String,Map<String,List<String>>> getEnsState(){
        List<EnsRegister> list = ensRegisterMapper.selectAll();
        Map<String, Map<String, List<String>>> map = new LinkedHashMap<>();
        if(CollectionUtils.isEmpty(list)){
            return map;
        }
        for (EnsRegister ensRegister : list) {
            String state = ensRegister.getState();
            Map<String,List<String>>  chainStateMap ;
            if(null == map.get(state)){
                chainStateMap = new LinkedHashMap<>();
                map.put(state,chainStateMap);
            }
            chainStateMap = map.get(state);
            String chainState = ensRegister.getChainState();
            if(null == chainStateMap.get(chainState)){
               List<String> ensList = new ArrayList<>();
               chainStateMap.put(chainState,ensList);
            }
            List<String> ensList = chainStateMap.get(chainState);
            ensList.add(ensRegister.getEnsName());


        }
        return map;
    }



    private GasPriceRes getGasPrice(){
        GasPriceRes gasPriceRes = null;
        for (int i = 0; i < 5; i++) {
            try {
                gasPriceRes =  ethIntegration.getGasPrice();
                return gasPriceRes;
            } catch (Exception e) {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException ex) {
                    continue;
                }
                continue;
            }

        }
        return null;

    }
    private GasPriceDto getGasPriceDto(String txSpeed){
        GasPriceRes gasPriceRes = getGasPrice();
        if(null == gasPriceRes){
            return  null;
        }
        GasPriceDto gasPriceDto = new GasPriceDto();
        gasPriceDto.setCurrentBaseFee(Convert.toWei(gasPriceRes.getCurrentBaseFee(), Convert.Unit.GWEI).toBigInteger());
        gasPriceDto.setRecommendedBaseFee(Convert.toWei(gasPriceRes.getRecommendedBaseFee(), Convert.Unit.GWEI).toBigInteger());
        switch (txSpeed){
            case "save_low":
                gasPriceDto.setMaxPriorFee(Convert.toWei(gasPriceRes.getSafeLow(), Convert.Unit.GWEI).toBigInteger());

                break;
            case "normal":
                gasPriceDto.setMaxPriorFee(Convert.toWei(gasPriceRes.getStandard(), Convert.Unit.GWEI).toBigInteger());

                break;
                case "fast":
                    gasPriceDto.setMaxPriorFee(Convert.toWei(gasPriceRes.getFast(), Convert.Unit.GWEI).toBigInteger());

                    break;
            case "fastest":
                gasPriceDto.setMaxPriorFee(Convert.toWei(gasPriceRes.getFastest(), Convert.Unit.GWEI).toBigInteger());

                break;
            default:
                return null;

        }
        gasPriceDto.setMaxFee(new BigDecimal(gasPriceDto.getCurrentBaseFee()).multiply(new BigDecimal("1.25")).toBigInteger());
        return gasPriceDto;
    }

}
